"""
CP-SAT求解器模块

使用Google OR-Tools的CP-SAT求解器进行项目调度优化。
实现约束建模、求解和结果处理的核心算法。
"""

import time
from typing import Any, Dict, List, Optional, Tuple, Union

from ortools.sat.python import cp_model

from .data_models import SolverResult
from .exceptions import ModelingError, SolvingError

# 扩展性类型定义
TimeRange = Tuple[int, int]  # (start_time, end_time)
WeatherInterruption = Dict[str, Union[TimeRange, str, bool]]  # 天气中断定义
MultiObjective = Dict[str, Union[float, int]]  # 多目标权重定义


class ProjectScheduleSolver:
    """项目调度CP-SAT求解器"""

    def __init__(self) -> None:
        """初始化求解器"""
        self.model: Optional[cp_model.CpModel] = None
        self.solver: Optional[cp_model.CpSolver] = None
        self.activity_vars: Optional[Dict[int, Dict[str, Any]]] = None

    def solve_with_cpsat(
        self,
        project_data: Dict[str, Any],
        interruptions: Optional[List[WeatherInterruption]] = None,
        multi_objective_weights: Optional[MultiObjective] = None,
        solver_params: Optional[Dict[str, Any]] = None,
    ) -> SolverResult:
        """
        使用CP-SAT求解项目调度问题

        Args:
            project_data: 标准化项目数据，包含activities、precedences、resources、resource_demands
            interruptions: 天气中断列表，每个中断包含时间范围和影响的工序类型
                格式: [{"time_range": (start, end), "affected_types": ["outdoor"], "severity": 0.5}]
            multi_objective_weights: 多目标优化权重，如 {"makespan": 1.0, "cost": 0.3, "resource_leveling": 0.2}
            solver_params: 求解器参数配置，如 {"max_time_in_seconds": 300, "num_search_workers": 4}

        Returns:
            求解结果对象

        Raises:
            ModelingError: 模型构建错误
            SolvingError: 求解过程错误

        Note:
            当前版本仅实现基础调度优化，interruptions和multi_objective_weights参数为未来扩展预留
        """
        try:
            # 验证输入数据
            self._validate_project_data(project_data)

            # 验证扩展参数
            self._validate_extension_params(
                interruptions, multi_objective_weights, solver_params
            )

            # 构建CP-SAT模型
            self._build_model(project_data, interruptions, multi_objective_weights)

            # 求解模型
            return self._solve_model(project_data, solver_params)

        except Exception as e:
            if isinstance(e, (ModelingError, SolvingError)):
                raise
            else:
                raise SolvingError(f"求解过程发生未知错误: {str(e)}")

    def _validate_extension_params(
        self,
        interruptions: Optional[List[WeatherInterruption]],
        multi_objective_weights: Optional[MultiObjective],
        solver_params: Optional[Dict[str, Any]],
    ) -> None:
        """
        验证扩展参数的有效性

        Args:
            interruptions: 天气中断列表
            multi_objective_weights: 多目标权重
            solver_params: 求解器参数

        Raises:
            ModelingError: 参数验证失败

        Note:
            当前版本仅进行基础验证，未来版本将实现完整的参数处理
        """
        # 验证天气中断参数
        if interruptions is not None:
            for i, interruption in enumerate(interruptions):
                if not isinstance(interruption, dict):
                    raise ModelingError(f"天气中断 {i} 必须是字典格式")

                required_keys = ["time_range", "affected_types"]
                for key in required_keys:
                    if key not in interruption:
                        raise ModelingError(f"天气中断 {i} 缺少必需字段: {key}")

                # 验证时间范围
                time_range = interruption["time_range"]
                if not isinstance(time_range, (list, tuple)) or len(time_range) != 2:
                    raise ModelingError(f"天气中断 {i} 的时间范围格式无效")

                start, end = time_range
                if (
                    not isinstance(start, int)
                    or not isinstance(end, int)
                    or start >= end
                ):
                    raise ModelingError(f"天气中断 {i} 的时间范围无效: {time_range}")

        # 验证多目标权重
        if multi_objective_weights is not None:
            valid_objectives = ["makespan", "cost", "resource_leveling", "quality"]
            for objective, weight in multi_objective_weights.items():
                if objective not in valid_objectives:
                    raise ModelingError(f"不支持的优化目标: {objective}")
                if not isinstance(weight, (int, float)) or weight < 0:
                    raise ModelingError(f"优化目标 {objective} 的权重必须为非负数")

        # 验证求解器参数
        if solver_params is not None:
            valid_params = [
                "max_time_in_seconds",
                "num_search_workers",
                "log_search_progress",
                "random_seed",
            ]
            for param, value in solver_params.items():
                if param not in valid_params:
                    raise ModelingError(f"不支持的求解器参数: {param}")

    def _build_model(
        self,
        project_data: Dict[str, Any],
        interruptions: Optional[List[WeatherInterruption]] = None,
        multi_objective_weights: Optional[MultiObjective] = None,
    ) -> None:
        """构建CP-SAT模型"""
        try:
            # 创建CP-SAT模型
            self.model = cp_model.CpModel()

            # 提取数据
            activities = project_data["activities"]
            precedences = project_data["precedences"]
            resources = project_data["resources"]
            resource_demands = project_data["resource_demands"]

            # 计算时间窗口上界
            horizon = self._calculate_horizon(activities)

            # 创建工序变量
            self.activity_vars = self._create_activity_variables(activities, horizon)

            # 添加前后置关系约束
            self._add_precedence_constraints(precedences, self.activity_vars)

            # 添加资源约束
            self._add_resource_constraints(
                resources, resource_demands, activities, self.activity_vars
            )

            # 添加天气中断约束（未来扩展）
            if interruptions is not None:
                self._add_weather_constraints(
                    interruptions, activities, self.activity_vars
                )

            # 设置优化目标
            if multi_objective_weights is not None:
                self._set_multi_objective(self.activity_vars, multi_objective_weights)
            else:
                self._set_objective(self.activity_vars)

        except Exception as e:
            raise ModelingError(f"模型构建失败: {str(e)}")

    def _create_activity_variables(self, activities: List[Dict], horizon: int) -> Dict:
        """
        创建工序变量

        为每个工序创建开始时间、结束时间和区间变量

        Args:
            activities: 工序列表
            horizon: 时间窗口上界

        Returns:
            包含所有工序变量的字典
        """
        if self.model is None:
            raise ModelingError("模型未初始化")

        activity_vars = {}

        for activity in activities:
            activity_id = activity["id"]
            duration = activity["duration"]

            # 创建开始时间变量
            start_var = self.model.NewIntVar(0, horizon, f"start_{activity_id}")

            # 创建结束时间变量
            end_var = self.model.NewIntVar(0, horizon, f"end_{activity_id}")

            # 添加持续时间约束: end = start + duration
            self.model.Add(end_var == start_var + duration)

            # 创建区间变量
            interval_var = self.model.NewIntervalVar(
                start_var, duration, end_var, f"interval_{activity_id}"
            )

            activity_vars[activity_id] = {
                "start": start_var,
                "end": end_var,
                "interval": interval_var,
                "duration": duration,
            }

        return activity_vars

    def _add_precedence_constraints(
        self, precedences: List[Dict], activity_vars: Dict
    ) -> None:
        """
        添加前后置关系约束

        支持四种依赖类型：FS, SS, FF, SF

        Args:
            precedences: 前后置关系列表
            activity_vars: 工序变量字典
        """
        if self.model is None:
            raise ModelingError("模型未初始化")

        for precedence in precedences:
            from_id = precedence["from"]
            to_id = precedence["to"]
            dep_type = precedence["type"]
            lag = precedence["lag"]

            # 验证工序ID存在
            if from_id not in activity_vars:
                raise ModelingError(f"前置工序 {from_id} 不存在")
            if to_id not in activity_vars:
                raise ModelingError(f"后续工序 {to_id} 不存在")

            pred_vars = activity_vars[from_id]
            succ_vars = activity_vars[to_id]

            # 根据依赖类型添加约束
            if dep_type == "FS":
                # Finish-to-Start: 后续开始时间 >= 前置结束时间 + 延时
                if lag >= 0:
                    self.model.Add(succ_vars["start"] >= pred_vars["end"] + lag)
                else:
                    self.model.Add(succ_vars["start"] + (-lag) >= pred_vars["end"])

            elif dep_type == "SS":
                # Start-to-Start: 后续开始时间 >= 前置开始时间 + 延时
                if lag >= 0:
                    self.model.Add(succ_vars["start"] >= pred_vars["start"] + lag)
                else:
                    self.model.Add(succ_vars["start"] + (-lag) >= pred_vars["start"])

            elif dep_type == "FF":
                # Finish-to-Finish: 后续结束时间 >= 前置结束时间 + 延时
                if lag >= 0:
                    self.model.Add(succ_vars["end"] >= pred_vars["end"] + lag)
                else:
                    self.model.Add(succ_vars["end"] + (-lag) >= pred_vars["end"])

            elif dep_type == "SF":
                # Start-to-Finish: 后续结束时间 >= 前置开始时间 + 延时
                if lag >= 0:
                    self.model.Add(succ_vars["end"] >= pred_vars["start"] + lag)
                else:
                    self.model.Add(succ_vars["end"] + (-lag) >= pred_vars["start"])

            else:
                raise ModelingError(f"不支持的依赖类型: {dep_type}")

    def _add_resource_constraints(
        self,
        resources: Dict[str, int],
        resource_demands: Dict[str, Dict],
        activities: List[Dict],
        activity_vars: Dict,
    ) -> None:
        """
        添加资源约束

        为每种资源类型添加Cumulative约束，确保任何时间点的资源使用量
        不超过可用资源总量

        Args:
            resources: 可用资源字典 {resource_type: capacity}
            resource_demands: 资源需求字典 {activity_id: {resource_type: demand}}
            activities: 工序列表
            activity_vars: 工序变量字典
        """
        if self.model is None:
            raise ModelingError("模型未初始化")

        # 为每种资源类型添加约束
        for resource_type, capacity in resources.items():
            intervals = []
            demands = []

            # 收集使用该资源的工序
            for activity in activities:
                activity_id = str(activity["id"])

                # 检查该工序是否需要此资源
                if activity_id in resource_demands:
                    demand = resource_demands[activity_id].get(resource_type, 0)

                    if demand > 0:
                        # 验证需求不超过总容量
                        if demand > capacity:
                            raise ModelingError(
                                f"工序 {activity_id} 对资源 {resource_type} 的需求 ({demand}) "
                                f"超过了可用容量 ({capacity})"
                            )

                        intervals.append(activity_vars[activity["id"]]["interval"])
                        demands.append(demand)

            # 如果有工序使用该资源，添加累积约束
            if intervals:
                self.model.AddCumulative(intervals, demands, capacity)

    def _set_objective(self, activity_vars: Dict) -> None:
        """
        设置优化目标

        最小化总工期（makespan）- 所有工序中最晚的结束时间

        Args:
            activity_vars: 工序变量字典
        """
        if self.model is None:
            raise ModelingError("模型未初始化")

        # 创建makespan变量，它必须大于等于所有工序的结束时间
        makespan = self.model.NewIntVar(0, 10000, "makespan")

        # 添加约束：makespan >= 每个工序的结束时间
        for activity_id, vars_dict in activity_vars.items():
            self.model.Add(makespan >= vars_dict["end"])

        # 设置优化目标：最小化makespan
        self.model.Minimize(makespan)

    def _add_weather_constraints(
        self,
        interruptions: List[WeatherInterruption],
        activities: List[Dict],
        activity_vars: Dict[int, Dict[str, Any]],
    ) -> None:
        """
        添加天气中断约束（未来扩展功能）

        Args:
            interruptions: 天气中断列表
            activities: 工序列表
            activity_vars: 工序变量字典

        Note:
            当前版本为接口预留，未来版本将实现完整的天气约束逻辑
        """
        if self.model is None:
            raise ModelingError("模型未初始化")

        # TODO: 实现天气中断约束
        # 1. 识别受影响的室外工序
        # 2. 在中断时间段内禁止室外工序执行
        # 3. 考虑天气严重程度对工序效率的影响

        # 示例实现框架（未来完善）:
        for interruption in interruptions:
            time_range = interruption["time_range"]
            affected_types = interruption.get("affected_types", ["outdoor"])
            severity = interruption.get("severity", 1.0)

            start_time, end_time = time_range

            # 找到受影响的工序
            affected_activities = []
            for activity in activities:
                if activity.get("is_outdoor", False) and "outdoor" in affected_types:
                    affected_activities.append(activity["id"])

            # 为受影响的工序添加约束（示例）
            for activity_id in affected_activities:
                if activity_id in activity_vars:
                    # 约束：工序不能在中断时间段内执行
                    # 这里需要更复杂的逻辑来处理时间段重叠
                    pass

    def _set_multi_objective(
        self, activity_vars: Dict[int, Dict[str, Any]], weights: MultiObjective
    ) -> None:
        """
        设置多目标优化（未来扩展功能）

        Args:
            activity_vars: 工序变量字典
            weights: 多目标权重

        Note:
            当前版本为接口预留，未来版本将实现完整的多目标优化
        """
        if self.model is None:
            raise ModelingError("模型未初始化")

        # TODO: 实现多目标优化
        # 1. 计算各个目标函数
        # 2. 根据权重组合目标函数
        # 3. 设置组合目标为优化目标

        # 当前仍使用单目标优化（最小化工期）
        makespan = self.model.NewIntVar(0, 10000, "makespan")
        for activity_id, vars_dict in activity_vars.items():
            self.model.Add(makespan >= vars_dict["end"])

        # 未来可以添加其他目标，如：
        # - 成本最小化
        # - 资源平衡
        # - 质量最大化

        # 示例多目标组合（未来实现）:
        objectives = []

        # 工期目标
        if "makespan" in weights:
            objectives.append(("makespan", makespan, weights["makespan"]))

        # 成本目标（未来实现）
        if "cost" in weights:
            # cost_var = self._calculate_total_cost(activity_vars)
            # objectives.append(("cost", cost_var, weights["cost"]))
            pass

        # 资源平衡目标（未来实现）
        if "resource_leveling" in weights:
            # leveling_var = self._calculate_resource_leveling(activity_vars)
            # objectives.append(("leveling", leveling_var, weights["resource_leveling"]))
            pass

        # 当前版本仍使用单目标
        self.model.Minimize(makespan)

    def _extract_solution(
        self,
        activity_vars: Dict,
        activities: List[Dict],
        resource_demands: Dict[str, Dict[str, int]],
    ) -> Dict[int, Dict[str, Any]]:
        """
        提取求解结果

        从CP-SAT求解器中提取每个工序的调度信息

        Args:
            activity_vars: 工序变量字典
            activities: 工序列表
            resource_demands: 资源需求字典

        Returns:
            详细的调度结果字典
        """
        if self.solver is None:
            raise SolvingError("求解器未初始化")

        schedule = {}

        # 创建活动ID到名称的映射
        activity_names = {
            activity["id"]: activity.get("name", f'Activity_{activity["id"]}')
            for activity in activities
        }

        for activity_id, vars_dict in activity_vars.items():
            start_time = self.solver.Value(vars_dict["start"])
            end_time = self.solver.Value(vars_dict["end"])
            duration = vars_dict["duration"]

            # 获取资源需求信息
            activity_id_str = str(activity_id)
            resource_info = resource_demands.get(activity_id_str, {})
            resource_str = ", ".join(
                [f"{k}:{v}" for k, v in resource_info.items() if v > 0]
            )

            schedule[activity_id] = {
                "id": activity_id,
                "name": activity_names.get(activity_id, f"Activity_{activity_id}"),
                "start_time": start_time,
                "end_time": end_time,
                "duration": duration,
                "resource_info": resource_str,
            }

        return schedule

    def _validate_project_data(self, project_data: Dict[str, Any]) -> None:
        """
        验证项目数据的完整性

        Args:
            project_data: 项目数据字典

        Raises:
            ModelingError: 数据验证失败
        """
        required_keys = ["activities", "precedences", "resources", "resource_demands"]

        for key in required_keys:
            if key not in project_data:
                raise ModelingError(f"缺少必需的数据字段: {key}")

        activities = project_data["activities"]
        if not activities:
            raise ModelingError("工序列表不能为空")

        # 验证工序数据
        for activity in activities:
            if "id" not in activity or "duration" not in activity:
                raise ModelingError(f"工序数据不完整: {activity}")

            if activity["duration"] <= 0:
                raise ModelingError(f"工序 {activity['id']} 的工期必须大于0")

    def _calculate_horizon(self, activities: List[Dict]) -> int:
        """
        计算时间窗口上界

        使用所有工序工期之和作为保守估计

        Args:
            activities: 工序列表

        Returns:
            时间窗口上界
        """
        total_duration = sum(activity["duration"] for activity in activities)
        # 添加一些缓冲，确保有足够的时间窗口
        buffer = max(10, total_duration // 10)
        horizon = total_duration + buffer
        return int(horizon)

    def _solve_model(
        self,
        project_data: Dict[str, Any],
        solver_params: Optional[Dict[str, Any]] = None,
    ) -> SolverResult:
        """
        求解CP-SAT模型

        Args:
            project_data: 项目数据

        Returns:
            求解结果
        """
        if self.model is None:
            raise ModelingError("模型未初始化")

        try:
            # 创建求解器
            self.solver = cp_model.CpSolver()

            # 配置求解器参数
            if solver_params is not None:
                self._configure_solver_parameters(solver_params)

            # 记录求解开始时间
            start_time = time.time()

            # 求解模型
            status = self.solver.Solve(self.model)

            # 计算求解时间
            solve_time = time.time() - start_time

            # 处理求解结果
            return self._process_solve_result(status, solve_time, project_data)

        except Exception as e:
            raise SolvingError(f"求解过程失败: {str(e)}")

    def _configure_solver_parameters(self, solver_params: Dict[str, Any]) -> None:
        """
        配置求解器参数

        Args:
            solver_params: 求解器参数字典

        Note:
            支持的参数包括时间限制、并行度、日志等
        """
        if self.solver is None:
            raise SolvingError("求解器未初始化")

        # 设置时间限制
        if "max_time_in_seconds" in solver_params:
            self.solver.parameters.max_time_in_seconds = solver_params[
                "max_time_in_seconds"
            ]

        # 设置并行搜索线程数
        if "num_search_workers" in solver_params:
            self.solver.parameters.num_search_workers = solver_params[
                "num_search_workers"
            ]

        # 设置搜索日志
        if "log_search_progress" in solver_params:
            self.solver.parameters.log_search_progress = solver_params[
                "log_search_progress"
            ]

        # 设置随机种子
        if "random_seed" in solver_params:
            self.solver.parameters.random_seed = solver_params["random_seed"]

    def _process_solve_result(
        self, status: int, solve_time: float, project_data: Dict[str, Any]
    ) -> SolverResult:
        """
        处理求解结果

        Args:
            status: CP-SAT求解状态
            solve_time: 求解时间
            project_data: 项目数据

        Returns:
            格式化的求解结果
        """
        if self.solver is None:
            raise SolvingError("求解器未初始化")
        if self.activity_vars is None:
            raise SolvingError("工序变量未初始化")

        # 状态映射
        status_map = {
            cp_model.OPTIMAL: "OPTIMAL",
            cp_model.FEASIBLE: "FEASIBLE",
            cp_model.INFEASIBLE: "INFEASIBLE",
            cp_model.MODEL_INVALID: "MODEL_INVALID",
            cp_model.UNKNOWN: "UNKNOWN",
        }

        status_str = status_map.get(status, "UNKNOWN")

        # 如果有可行解，提取解决方案
        if status in [cp_model.OPTIMAL, cp_model.FEASIBLE]:
            makespan = self.solver.ObjectiveValue()
            schedule = self._extract_solution(
                self.activity_vars,
                project_data["activities"],
                project_data["resource_demands"],
            )

            return SolverResult(
                status=status_str,
                makespan=int(makespan),
                schedule=schedule,
                solve_time=solve_time,
            )
        else:
            # 无解或其他错误状态
            error_messages = {
                "INFEASIBLE": "问题无解，请检查约束条件是否过于严格",
                "MODEL_INVALID": "模型构建错误，请检查输入数据",
                "UNKNOWN": "求解器无法确定问题状态",
            }

            return SolverResult(
                status=status_str,
                solve_time=solve_time,
                error_message=error_messages.get(status_str, "未知错误"),
            )


def solve_with_cpsat(
    project_data: Dict[str, Any],
    interruptions: Optional[List[WeatherInterruption]] = None,
    multi_objective_weights: Optional[MultiObjective] = None,
    solver_params: Optional[Dict[str, Any]] = None,
) -> SolverResult:
    """
    便捷函数：使用CP-SAT求解项目调度问题

    Args:
        project_data: 标准化项目数据
        interruptions: 天气中断列表(未来扩展)
        multi_objective_weights: 多目标优化权重(未来扩展)
        solver_params: 求解器参数配置

    Returns:
        求解结果对象
    """
    solver = ProjectScheduleSolver()
    return solver.solve_with_cpsat(
        project_data, interruptions, multi_objective_weights, solver_params
    )
